﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using JetBlack.Diagnostics;
using JetBlack.AuthFeedBus.Messages;

namespace JetBlack.AuthFeedBus.Distributor
{
    internal class Acceptor : IDisposable
    {
        public delegate void OnClientOpenedHandler(Interactor client);
        public event OnClientOpenedHandler OnClientOpened;

        public delegate void OnClientClosedHandler(Interactor client);
        public event OnClientClosedHandler OnClientClosed;

        public delegate void OnClientMessageHandler(Interactor client, Message message);
        public event OnClientMessageHandler OnClientMessage;

        private readonly Thread readerThread, writerThread;
        private readonly AutoResetEvent writeEvent = new AutoResetEvent(false);
        private int nextInteractorId = 0;
        private bool isListening = true;
        private readonly Socket listener;
        private readonly Dictionary<Socket, Interactor> clients = new Dictionary<Socket, Interactor>();

        public Acceptor()
        {
            readerThread = new Thread(RunReader);
            writerThread = new Thread(RunWriter);
            listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        }

        public void Start()
        {
            readerThread.Start();
            writerThread.Start();
        }

        public void RunWriter()
        {
            while (writeEvent.WaitOne())
            {
                if (!IsListening)
                    return;

                List<Socket> checkWrite = new List<Socket>();
                lock (clients)
                {
                    foreach (Interactor client in clients.Values)
                        if (client.HasMessageToWrite)
                            checkWrite.Add(client.Socket);
                }

                if (checkWrite.Count == 0)
                    continue;

                Socket.Select(null, checkWrite, null, -2);

                foreach (Socket socket in checkWrite)
                {
                    Interactor client;
                    lock (clients)
                    {
                        clients.TryGetValue(socket, out client);
                    }

                    try
                    {
                        if (client != null)
                            client.WriteMessage();
                    }
                    catch (Exception ex)
                    {
                        AppEnvironment.TraceSource.Error("Failed to write message to client {0} - {1}", client, ex.Message);
                    }
                }
            }
        }

        public void RunReader()
        {
            AppEnvironment.TraceSource.Verbose("Accepting clients");

            try
            {
                var hostEntry = Dns.GetHostEntry(String.Empty);
                IPEndPoint endPoint = new IPEndPoint(hostEntry.AddressList.First(x => x.AddressFamily == AddressFamily.InterNetwork), AppEnvironment.Configuration.Port);
                listener.Bind(endPoint);
                listener.Listen(10);

                while (IsListening)
                {
                    List<Socket> checkRead = new List<Socket>();
                    checkRead.Add(listener);
                    lock (clients)
                    {
                        foreach (Socket socket in clients.Keys)
                            checkRead.Add(socket);
                    }

                    Socket.Select(checkRead, null, null, -2);

                    foreach (Socket socket in checkRead)
                    {
                        if (socket == null)
                            continue;
                        else if (socket == listener)
                            HandleClientOpened();
                        else
                            HandleClientRead(socket);
                    }
                }
            }
            catch (Exception ex)
            {
                AppEnvironment.TraceSource.Error(ex);
            }

            Socket[] sockets;
            lock (clients)
            {
                sockets = clients.Keys.ToArray();
            }

            foreach (Socket socket in sockets)
                socket.Close();
        }

        private void HandleClientRead(Socket socket)
        {
            lock (clients)
            {
                clients[socket].ReadMessage();
            }
        }

        private void HandleClientOpened()
        {
            if (!IsListening)
                return;

            Socket socket = listener.Accept();

            Interactor interactor = new Interactor(socket, nextInteractorId++);
            interactor.OnClosed += HandleClientClosed;
            interactor.OnMessageReceived += HandleClientMessage;
            interactor.OnWriteRequest += HandleWriteRequest;

            if (OnClientOpened != null)
                OnClientOpened(interactor);

            AppEnvironment.TraceSource.Verbose("Client connection received for {0}", interactor);

            lock (clients)
            {
                clients.Add(socket, interactor);
            }
        }
        private void HandleClientMessage(Interactor client, Message message)
        {
            if (OnClientMessage != null)
                OnClientMessage(client, message);
        }

        private void HandleClientClosed(Interactor client)
        {
            AppEnvironment.TraceSource.Verbose("Client closed connection {0}", client);
            if (OnClientClosed != null)
                OnClientClosed(client);
            client.Dispose();

            lock (clients)
            {
                clients.Remove(client.Socket);
            }
        }

        private void HandleWriteRequest(Interactor client)
        {
            writeEvent.Set();
        }

        public bool IsListening
        {
            get { lock (this) return isListening; }
            set { lock (this) isListening = value; }
        }


        public void Dispose()
        {
            IsListening = false;
            //foreach (Socket socket in clients.Keys.ToArray())
            //    socket.Disconnect(false);
            listener.Close();

            writeEvent.Set();
            readerThread.Join();
            writerThread.Join();
        }
    }
}
